// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

var util = require('util');
var assert = require('assert');
var fs = require('fs');

var libclang = require('libclang');

var Cursor = libclang.Cursor;
var Type = libclang.Type;

var utils = require('./utils.js');
var marshalers = require('./marshalers.js');

var indent = 0;
var prefixes = ["", "  "];

var scriptPath = process.argv[1]
var inputPath = process.argv[2];
var outputPath = process.argv[3];
var llvmargs = process.argv.slice(4);

if (!inputPath || !outputPath || !llvmargs.length) {
  console.error("Usage: %s <inputfile> <outputfile> <llvmargs>", scriptPath);
  process.exit(0);
}

console.log("writing bindings to %s", outputPath);

var out = fs.createWriteStream(outputPath);

function __ (/* fmt, args */) {
  var str = util.format.apply(util, arguments);
  if (indent === 0) out.write('\n\n');
  if (str[0] === "}") indent--;
  if (!(indent in prefixes)) prefixes[indent] = prefixes[indent - 1] + prefixes[1];
  out.write(prefixes[indent]);
  out.write(str);
  out.write('\n');
  if (str[str.length - 1] === "{") indent++;
}

out.write(fs.readFileSync(inputPath));

out.write("\n// AUTOGENERATED FILE. DO NOT EDIT!\n\n");

var NODE_INCLUDE_DIR = require('path').join(process.execPath, '..', '..', 'include', 'node');

var tu = libclang.Parse(['-x',
                         'c++',
                         inputPath,
                         '-I' + NODE_INCLUDE_DIR].concat(llvmargs));
var root = tu.cursor();

var classes2bind = [];

var global_functions = Object.create(null);

root.visit(function (cursor) {
  if (cursor.kind() === Cursor.VarDecl &&
      cursor.type().declaration().spelling() === 'Wrapper') {
    var arg = utils.guessFirstTemplateArgument(cursor);
    assert(arg !== null, 'Failed to guess Wrapper type argument');
    classes2bind.push(arg.definition());
  } else if (cursor.kind() === Cursor.FunctionDecl) {
    global_functions[cursor.spelling()] = true;
  }
  return Cursor.VisitContinue;
});

function Method (name, is_constructor, is_static, rawSignature, signature) {
  this.name_ = name;
  this.is_constructor_ = is_constructor;
  this.is_static_ = is_static;
  this.rawSignature_ = rawSignature;
  this.signature_ = signature;
}

Method.prototype.toString = function () {
  var reqArgs = this.rawSignature_.args.slice(0, this.rawSignature_.required);
  var defArgs = this.rawSignature_.args.slice(this.rawSignature_.required);
  var need_separator = defArgs.length != 0 && reqArgs.length != 0;
  return "%s %s (%s%s%s);".format(
    type2string(this.rawSignature_.result),
    this.name_,
    reqArgs.map(type2string).join(', '),
    need_separator ? ", " : "",
    (defArgs.length != 0) ? ('[' + defArgs.map(type2string).join(', ') + ']') : ''
  );
};

Method.prototype.equals = function (other) {
  if (this.signature_.required !== other.signature_.required) return false;

  var required = this.signature_.required;

  for (var i = 0; i < required; i++) {
    if (this.signature_.args[i] !== other.signature_.args[i]) return false;
  }

  return true;
};

function countNonSynthetic(args) {
  var argc = 0;
  for (var i = 0; i < args.length; i++) if (!marshalers.isSynthetic(args[i])) argc++;
  return argc;
}

function tryBindMethod(host, method) {
  var is_constructor = method.kind() === Cursor.Constructor;
  var is_static = method.kind() === Cursor.CXXMethod ? method.isStatic() : true;
  var signature = method.type();

  if (signature.isVariadic()) return;  // TODO support variadic methods

  var result = signature.result();
  var args = signature.args();

  var reqArgc = utils.guessRequiredArgs(method);

  var params = utils.children(method).filter(function (c) { return c.kind() === Cursor.ParmDecl; });
  assert(params.length === args.length);

  var rawSignature = { result: result, args: args, required: reqArgc };

  var signature = {
    result: marshalers.marshalType(result, "toV8"),
    args: args.map(function (t, idx) {
      return marshalers.marshalType(t, "fromV8", params[idx]);
    }),
    required: reqArgc
  };

  for (var i = signature.required; i < signature.args.length; i++) {
    if (!marshalers.canMarshalFromV8(signature.args[i])) {
      signature.args = signature.args.slice(0, i);
      break;
    }
  }

  if (marshalers.canMarshalToV8(signature.result) &&
      signature.args.every(marshalers.canMarshalFromV8)) {
    var name = is_constructor ? host.name : method.spelling();
    var method = new Method(name, is_constructor, is_static, rawSignature, signature);

    var methods = host.methods[name] || (host.methods[name] = []);

    for (var i = 0; i < methods.length; i++) if (methods[i].equals(method)) return;

    methods.push(method)
  }
};

function type2string(type) {
  var type = type.canonical();
  switch (type.kind()) {
  case Type.Pointer:
    return type2string(type.pointee()) + "*";
  case Type.LValueReference:
    return type2string(type.pointee()) + "&";
  case Type.Record:
    return type.declaration().spelling();
  default:
    return type.spelling();
  }
}

var classes = classes2bind.map(function (decl) { return marshalers.addBoundClass(decl); });

classes.forEach(function (clazz) {
  clazz.methods = Object.create(null);

  var access = Cursor.CXXPrivate;
  clazz.decl.visit(function (cursor) {
    switch (cursor.kind()) {
    case Cursor.CXXAccessSpecifier:
      access = cursor.access();
      break;
    case Cursor.CXXMethod:
      if (access === Cursor.CXXPublic) {
        var name = cursor.spelling();
        var method_name = "%s_%s".format(clazz.name, name);
        if (method_name in global_functions && !(name in clazz.methods)) {
          clazz.methods[name] = [new Method(name, false, cursor.isStatic(), null, null)];
        } else {
          tryBindMethod(clazz, cursor);
        }
      }
      break;
    }
    return Cursor.VisitContinue;
  });
});

var LLVMNamespace = {
  name: 'LLVM',
  cxxname: 'llvm',
  methods: Object.create(null)
};

// TODO(vegorov): manually declaring namespaces to export here does not scale!
var IntrinsicNamespace = {
  name: 'Intrinsic',
  cxxname: 'llvm::Intrinsic',
  methods: Object.create(null)
};

function isOperator(cursor) {
  return /^operator\W+$/.test(cursor.spelling());
}

function isTemplate(cursor) {
  return /^\w+</.test(cursor.display());
}

root.visit(function (cursor) {
  if (cursor.kind() === Cursor.Namespace && cursor.spelling() === "llvm") {
    cursor.visit(function (cursor) {
      if (cursor.kind() === Cursor.FunctionDecl && !isOperator(cursor) && !isTemplate(cursor)) {
        tryBindMethod(LLVMNamespace, cursor);
      } else if (cursor.kind() === Cursor.Namespace && cursor.spelling() === "Intrinsic") {
        cursor.visit(function (cursor) {
          if (cursor.kind() === Cursor.FunctionDecl && !isOperator(cursor) && !isTemplate(cursor)) {
            tryBindMethod(IntrinsicNamespace, cursor);
          }
          return Cursor.VisitContinue;
        });
      }
      return Cursor.VisitContinue;
    });
  }
  return Cursor.VisitContinue;
});

function emitMethodCall(host, method, args) {
  var idx = 0;
  var margs = [];
  for (var i = 0; i < args.length; i++) {
    var arg = args[i];
    if (!marshalers.isSynthetic(arg)) {
      margs.push(arg.fromV8("args[%s]".format(idx++)));
    } else {
      margs.push(arg.synthesize());
    }
  }

  if (method.is_static_) {
    return method.signature_.result.toV8("%s::%s(%s)".format(host.cxxname, method.name_, margs.join(', ')))
  } else {
    return method.signature_.result.toV8("%s.Unwrap(args.This())->%s(%s)".format(host.name, method.name_, margs.join(', ')))
  }
}

var trie = require('./trie.js');
function emitOverloadSelection(host, methods) {
  function prepareArgs(m, l) {
    var args = m.signature_.args.slice(0, m.signature_.required);
    l -= countNonSynthetic(args);
    assert(l >= 0);

    for (var i = m.signature_.required; i < m.signature_.args.length && l > 0; i++) {
      var arg = m.signature_.args[i];
      args.push(arg);
      if (!marshalers.isSynthetic(arg)) l--;
    }

    return args;
  }

  function checkArg(t, argidx) {
    __ ("if (!(args.Length() > %d && %s)) return THROW_ERROR(\"argument #%d to %s expected to be %s\");",
        argidx,
        t.test("args[%d]".format(argidx)),
        argidx,
        host.name + "::" + methods[0].name_,
        t.toString());
  }

  function emitChoice(n, l) {
    if (n.val !== null) {
      __ ("if (args.Length() == %d) return %s;",
          l,
          emitMethodCall(host, n.val, prepareArgs(n.val, l)));
    }

    if (n.arr.length === 0) {
      return;
    } else if (n.arr.length === 1) {
      // If there is only one path segment we invert tests.
      var arr = n.arr[0];
      arr.seg.forEach(function (t, idx) { checkArg(t, l + idx); });
      emitChoice(arr.nod, l + arr.seg.length);
      return;
    }

    for (var i = 0; i < n.arr.length; i++) {
      var arr = n.arr[i];
      __ ("%sif (args.Length() > %d && %s) {", i > 0 ? "} else " : "", l, arr.seg[0].test("args[%d]".format(l)));
      for (var idx = 1; idx < arr.seg.length; idx++) {
        checkArg(arr.seg[idx], l + idx);
      }
      emitChoice(arr.nod, l + arr.seg.length);
    }

    __ ("}");
  }

  function filterSynthetic(args) { return args.filter(function (x) { return !marshalers.isSynthetic(x); }); }

  var root = new trie.Nod(null, []);
  var sigs = [];
  methods.forEach(function (m) {
    var args = filterSynthetic(m.signature_.args);
    var req = countNonSynthetic(m.signature_.args.slice(0, m.signature_.required));
    for (var argc = req; argc <= args.length; argc++) {
      try {
        root.insert(args.slice(0, argc), m);
      } catch (e) {}
    }
  });

  emitChoice(root, 0);
  __ ("return THROW_ERROR(\"failed to resolve overload of %s::%s\");", host.name, methods[0].name_);
}


function emitAllBoundMethods(host) {
  __ ("// ------------------------ %s ----------------------------", host.name);

  Object.keys(host.methods).forEach(function (method_name) {
    var methods = host.methods[method_name];

    var method_name = "%s_%s".format(host.name, method_name);

    if (method_name in global_functions) {
      console.log("Method %s bound manually.", method_name);
      return;
    }

    __ ("static v8::Handle<v8::Value> %s (const v8::Arguments& args) {", method_name);
    emitOverloadSelection(host, methods);
    __ ("}");
  });
}

classes.forEach(emitAllBoundMethods);
emitAllBoundMethods(LLVMNamespace);
emitAllBoundMethods(IntrinsicNamespace);

// Collect declarations of all enums that were mentioned in signatures of bound methods.
var used_enums = marshalers.Enum.getInstances().map(function (e) { return e.decl; });

classes.forEach(function (clazz) {
  function isStatic(name) {
    return clazz.methods[name][0].is_static_;
  }

  __ ("static void Bind%sMembers() {", clazz.name);
  __ ("// public methods");
  Object.keys(clazz.methods).forEach(function (method_name) {
    __ ("BIND_%s_METHOD(%s, %s, %s_%s);", isStatic(method_name) ? "STATIC" : "INSTANCE", clazz.name, method_name, clazz.name, method_name);
  });

  // Find used enums that belong to this class and bind their constants.
  var clazz_usr = clazz.decl.usr();
  function isNestedEnum(e) { return e.parent().usr() === clazz_usr; }

  used_enums.filter(isNestedEnum).forEach(function (e) {
    __ ("// enum %s", e.spelling());
    e.visit(function (n) {
      __ ("BIND_CONST(%s, %s, %s::%s);", clazz.name, n.spelling(), clazz.cxxname, n.spelling());
      return Cursor.VisitContinue;
    });
  });

  // Remove bound enums from the list of used.
  used_enums = used_enums.filter(function (e) { return !isNestedEnum(e); });

  __ ("}");

});

var bindGlobals = false;
if (used_enums.length > 0 ||
    Object.keys(LLVMNamespace.methods).length > 0 ||
    Object.keys(IntrinsicNamespace.methods).length > 0) {
  bindGlobals = true;
  __ ("static void BindGlobals(v8::Handle<v8::Object> G) {");
  __ ("v8::HandleScope scope;");

  __ ("// global functions");
  Object.keys(LLVMNamespace.methods).forEach(function (method_name) {
    __ ("SET_FUNCTION(G, %s, LLVM_%s);", method_name, method_name);
  });

  __ ("// Intrinsic namespace");
  __ ("{");
  __ ("v8::Local<v8::String> IntrinsicStr = v8::String::NewSymbol(\"Intrinsic\");");
  __ ("if (!G->Has(IntrinsicStr)) G->Set(IntrinsicStr, v8::Object::New());");
  __ ("v8::Local<v8::Object> Intrinsic = v8::Local<v8::Object>::Cast(G->Get(IntrinsicStr));");
  Object.keys(IntrinsicNamespace.methods).forEach(function (method_name) {
    __ ("SET_FUNCTION(Intrinsic, %s, Intrinsic_%s);", method_name, method_name);
  });
  __ ("}");

  used_enums.forEach(function (e) {
    __ ("// enum %s", e.spelling());
    var path = utils.pathTo(e);
    __ ("{");
    var prev = "G";
    for (var i = 1, l = path.length > 2 ? path.length - 1 : path.length; i < l; i++) {
      var id = path[i];
      __ ("v8::Local<v8::String> %sStr = v8::String::NewSymbol(\"%s\");", id, id);
      __ ("if (!%s->Has(%sStr)) %s->Set(%sStr, v8::Object::New());", prev, id, prev, id);
      __ ("v8::Local<v8::Object> %s = v8::Local<v8::Object>::Cast(%s->Get(%sStr));", id, prev, id);
      prev = id;
    }

    e.visit(function (n) {
      __ ("SET_CONSTANT(%s, %s, %s);", prev, n.spelling(), utils.cxxname(n)); return Cursor.VisitContinue;
    });
    __ ("}");
  });
  __ ("}");
}

__ ("void RegisterAllGeneratedBindings(v8::Handle<v8::Object> exports) {");
classes.forEach(function (clazz) {
  __ ("Bind%sMembers();", clazz.name);
  __ ("exports->Set(v8::String::New(\"%s\"), %s.Constructor());", clazz.name, clazz.name);
});
if (bindGlobals) {
  __ ("BindGlobals(exports);")
}
__ ("}");

out.end();
